Claude Code 核心解析

对话循环

异步生成器

Claude Code 的对话主循环是一个以 async function* 定义的异步生成器。它不是一次性执行完毕的普通函数,而是一个可暂停、可恢复、可取消的"活"的流程。每一次 yield 就像心跳的一次搏动,将流式事件推向调用方。

这个设计选择值得用更多篇幅来理解。在传统的编程模型中,函数调用是同步的:调用者发起请求,被调用者执行计算,返回结果。但 Agent 的交互模式打破了这种同步假设——模型可能需要几十秒才能完成一次响应,而且响应是逐 token 到达的;工具执行可能耗时数分钟,期间需要实时反馈进度;用户可能随时中断操作,要求立即停止。

面对这些需求,传统的函数调用模型力不从心。异步生成器提供了完美的答案:它像一个可以随时暂停和恢复的"协程",在"生产者"(对话循环)和"消费者"(UI 渲染层)之间建立了一条实时的事件管道。


整个对话循环的入口是一个导出的异步生成器函数,它接受一个参数对象,可向外产出五种类型的事件(流式事件、请求开始事件、消息、墓碑消息、工具调用摘要),最终返回一个表示对话终结状态的对象。

这个函数签名蕴含了三层设计决策:

  1. Yield 类型联合体(Union of yielded types):生成器可向外产出五种类型的事件——流式 token 到达事件、API 请求开始事件、用户/助手/系统消息、标记已废弃消息的墓碑消息、工具调用摘要消息。这五种事件覆盖了对话过程中所有需要传递给 UI 层的信息。使用联合类型而非多个独立的生成器,确保了事件的时序一致性——UI 看到的事件顺序与产生顺序完全一致。
  2. 返回类型 Terminal:生成器最终返回一个终结状态对象,表示对话的终结原因。调用方通过 for await (... of query(...)) 消费事件流,当循环自然结束时,生成器的 return 值即为终止原因。这种"yield 过程、return 结论"的模式使得上层代码可以清晰地分离"过程中的处理"和"结束后的收尾"。
  3. 参数对象:将所有入参封装在一个结构化对象中,而非散列参数,使得调用方可以按需提供字段。关键字段包括消息历史、系统提示词、权限检查函数、工具执行上下文、最大循环次数等。

为什么选择 AsyncGenerator 而非回调或 Promise?因为生成器天然适配"流式生产-流式消费"的模型。模型的响应是逐 token 到达的,工具的执行结果是逐步产出的,生成器的 yield 机制让每一层都可以做到"有数据就推送,没数据就等待",而不需要回调地狱或 Promise 链。

设计哲学对比: 如果使用回调模式,你需要为每种事件类型注册独立的回调函数,代码会变成分散在各处的回调处理逻辑。如果使用 Promise 链,虽然避免了回调地狱,但 Promise 是"一次性的"——它只能 resolve 一次,无法表达持续的事件流。如果使用 RxJS Observable,虽然功能强大,但引入了沉重的依赖和陡峭的学习曲线。AsyncGenerator 是"刚刚好"的方案——原生语言支持、零额外依赖、类型安全、天然支持流式和取消。

对话循环中流转的事件可分为以下几类,它们共同构成了对话过程的"心跳信号":

  • stream_request_start:每次 API 请求开始前发出,告知 UI 层一个新请求即将发起。在循环的每次迭代开头都会发出此事件。这个事件的实用价值在于 UI 可以展示"正在思考…"的状态指示器。
  • StreamEvent:来自 Anthropic API 的原始流式事件,包括文本块增量(content_block_delta)、thinking 块、tool_use 块等。这些事件直接从 API 响应流透传给 UI。想象你在看一场直播——StreamEvent 就是视频流中的每一帧画面,UI 层负责将这些画面拼接成流畅的视频。
  • Message:结构化的消息对象,包括 AssistantMessage(助手回复,可能包含 tool_use 块)、UserMessage(用户输入或 tool_result)、SystemMessage(系统通知)等。与 StreamEvent 不同,Message 是经过解析和结构化的——相当于直播结束后的回放,画面已被编辑和组织。
  • TombstoneMessage:当流式回退(streaming fallback)发生时,部分已产出的消息需要被标记为废弃。墓碑消息告诉 UI 移除对应的历史消息。这个名字来自程序员熟知的"墓碑标记"模式——就像墓碑标记了一个生命的终结,TombstoneMessage 标记了一条消息的"失效"。
  • ToolUseSummaryMessage:在一批工具执行完成后,异步生成的简要摘要,用于在 UI 中折叠展示工具调用结果。这对于长时间的 Agent 会话尤为重要——如果没有摘要,几十次工具调用的完整输出会淹没整个屏幕。

消息体系:

  • UserMessage:用户的输入消息,也承载工具执行结果(tool_result)。在 API 的视角里,工具结果总是以 user 角色发送。这个设计可能看起来违反直觉——为什么工具结果是"user"角色?原因是 API 协议层面只有三种角色(system/user/assistant),工具结果需要被模型"看到",所以必须以 user 角色发送。这是一个工程约束驱动设计决策的典型案例。
  • AssistantMessage:模型返回的消息,可能包含文本块和 tool_use 块。当模型检测到需要调用工具时,响应中会包含 type: 'tool_use' 的内容块。AssistantMessage 的关键特性是它可能同时包含文本和工具调用——模型可能先输出一段解释(“我需要查看你的文件”),然后附加一个工具调用。这种"边说边做"的模式让 Agent 的行为更加透明。
  • SystemMessage:系统级通知,如权限变更、模型回退提示等。不参与 API 通信,仅在 UI 展示。SystemMessage 是"第四面墙"——它不参与模型对话,但告诉用户系统内部正在发生什么。
  • AttachmentMessage:附件消息,承载文件变更通知、内存文件(CLAUDE.md)内容、任务通知等附加信息。
  • ProgressMessage:工具执行的进度消息,用于实时反馈工具运行状态(如 Bash 命令的输出流、文件读取进度等)。

完整的对话轮的拆解

现在让我们跟随一个完整的 Turn——从用户按下回车键到模型完成响应或决定调用工具——理解 queryLoop 函数内部的完整流程。

用医学来类比,一个 Turn 就像一次完整的诊断过程:医生(模型)先查看病历(上下文预处理),然后与病人交流(API 调用),可能需要安排检查(工具调用),拿到检查结果后(工具执行)做出诊断(最终回复)。如果检查结果不足以确诊,医生会安排更多检查(下一轮循环)。

阶段1:

queryLoop 是一个 while(true) 无限循环。每次迭代代表一次"模型调用 + 工具执行"的完整回合。在循环顶部,函数从状态对象中解构出当前迭代所需的变量,包括工具使用上下文、消息列表、自动压缩追踪、恢复计数器等。

状态对象是一个可变状态容器,包含跨迭代传递的全部状态:消息列表、工具上下文、自动压缩追踪、恢复计数器、turn 计数等。每次 continue 回到循环顶部时,都会写入一个新的状态对象。

这个设计的关键洞察是:状态在"读"和"写"之间有明确的分界线。 在每次迭代的开始,函数通过解构一次性读取所有需要的状态字段(快照语义);在迭代结束时,通过构造新对象一次性写入更新后的状态(原子更新语义)。这避免了在迭代过程中部分更新导致的不一致问题。

阶段2:

在调用模型之前,循环执行一系列预处理步骤。这些步骤构成了一个精心设计的"压缩管线",目的是在有限的上下文窗口中保留最有价值的信息:

  1. 工具结果预算:对过大的工具结果进行截断或持久化到磁盘,确保不超过上下文窗口限制。这类似于计算机科学中的"分页"机制——当数据太大无法全部放入内存(上下文窗口)时,将部分数据存储到磁盘,只保留摘要或引用。
  2. Snip 压缩:如果启用了历史裁剪功能,会对过长的历史消息进行裁剪。Snip 是最"粗暴"的压缩方式——直接截断消息内容。它通常用于处理工具返回的超长输出(如大型文件的完整内容)。
  3. Microcompact:在自动压缩之前进行轻量级压缩,利用缓存编辑技术减少 token 消耗。Microcompact 的精妙之处在于它是"缓存友好"的——它尽量复用 API 侧已缓存的 token,避免因压缩导致缓存全面失效。
  4. Context Collapse:上下文折叠是一种更细粒度的压缩策略,它在不丢失信息的情况下将连续消息折叠为紧凑视图。可以把 Context Collapse 想象为将一段对话中的"你好"、“好的”、"我明白了"这类确认性消息折叠为一行——信息不丢,但占用的空间更少。
  5. 系统提示组装:将基础系统提示与动态上下文(如当前工作目录、用户配置等)合并为完整的系统提示。这一步的设计直接影响了缓存命中率——如果组装顺序不稳定,每次调用生成的 Prompt 字节内容可能不同,导致缓存失效。
  6. Autocompact:如果上下文超过阈值,自动压缩机制会触发,将历史对话摘要为压缩后的消息,然后替换待发送的消息列表。Autocompact 是压缩管线的"最后一道防线"——当其他轻量级压缩手段都无法将上下文缩减到限制以内时,它会执行一次全量摘要。
  7. Token 阻断检查:如果 token 数超过硬性限制,直接返回错误消息,不再发起 API 调用。这是一个"快速失败"机制——与其发送一个注定会失败的 API 请求,不如在本地就阻止它。

最佳实践提示: 这七步管线的设计遵循了一个重要原则:压缩手段从轻量到重量排列,每一步都先尝试最小代价的方案。 这个原则在你自己构建 Agent 系统时也值得遵循——先用 Snip 裁剪过长内容,再用 Microcompact 减少缓存浪费,再用 Context Collapse 折叠冗余信息,最后才用 Autocompact 做全量摘要。因为每一步都会丢失一些信息,应该尽量延迟最"激进"的压缩手段的使用。

阶段3:

所有预处理完成后,进入核心的 API 调用阶段。这里使用注入的模型调用依赖发起流式请求,将组装好的消息列表、系统提示和工具定义传递给模型 API。模型调用函数返回一个异步生成器,逐个产出流式事件。每次收到事件,循环执行以下逻辑:

  • 如果事件包含 assistant 消息,将其加入助手消息数组。
  • 如果事件包含工具调用块,将其收集起来,并标记需要后续工具执行。
  • 如果启用了流式工具执行,在收到工具调用块时立即开始执行工具,而不必等待整个响应完成。

这个阶段的一个微妙之处在于:模型可能在一个响应中同时包含文本内容和工具调用。例如,模型可能先输出"我来看看你的 package.json 文件",然后附加一个 Read 工具调用。循环需要正确处理这种混合输出——既要 yield 文本事件让 UI 渲染,又要收集工具调用块为后续执行做准备。

阶段4:

当流式响应结束后,循环检查是否需要执行工具。如果模型没有请求调用工具,进入终止路径,检查各种退出条件(stop hooks、token budget 等)后返回。

如果模型请求了工具调用,循环执行工具:根据是否启用了流式执行,选择从流式执行器获取剩余结果,或使用传统的批量执行函数。

工具执行同样是一个异步生成器。每产出一个结果消息,循环就将其 yield 给上层消费者(UI),同时收集到工具结果数组中。

这个设计体现了一个重要的工程原则:"结果收集"和"结果传递"是解耦的。 工具结果既被收集到数组中用于下一轮 API 调用,又被 yield 给 UI 用于实时展示。这两个关注点通过同一个 yield 操作同时完成,避免了额外的状态同步逻辑。

阶段5:

工具执行完毕后,循环执行附件注入(内存文件、文件变更通知、排队命令等),然后将所有消息(原始消息 + 助手消息 + 工具结果)打包为新的状态对象,通过 continue 回到 while(true) 的顶部。

下一轮迭代将使用这个扩展后的消息列表重新调用模型,模型将看到之前的工具结果,然后决定是继续调用工具还是给出最终回复。

附件注入是一个容易忽视但非常重要的步骤。想象这样一个场景:在工具执行期间,用户修改了 CLAUDE.md 文件。如果不注入这个变更,模型在下一轮调用中可能基于过时的配置做出决策。附件注入确保了每一轮循环开始时,模型都拥有最新的环境信息。

对话循环的终止发生在多个位置,每个终止原因对应不同的系统状态和清理逻辑:

终止原因 触发条件 用户体验 设计意图
completed 模型正常回复且无工具调用 Agent 给出最终回复 正常的"成功完成"路径
aborted_streaming 用户中断(Ctrl+C) 操作立即停止 用户主动中断,需要即时响应
aborted_tools 工具执行期间中断 当前工具被取消,结果丢弃 工具执行可能耗时较长,需要中断支持
max_turns 达到最大循环次数 Agent 停止并说明原因 防止无限循环消耗 token
blocking_limit Token 数超过硬性限制 Agent 报错退出 硬性安全边界,防止 API 错误
prompt_too_long 上下文过长且恢复失败 Agent 报错退出 所有压缩手段已用尽
model_error API 调用异常 Agent 报错并展示错误信息 网络或服务端问题的优雅降级
stop_hook_prevented Stop hook 阻止继续 Agent 停止并说明原因 用户配置的自动停止条件
hook_stopped 工具 hook 阻止继续 Agent 停止并说明原因 外部 Hook 脚本的决定
image_error 图片尺寸/格式错误 Agent 报错退出 输入数据格式问题
  • 正常终止completed——Agent 完成了任务
  • 用户主动终止aborted_streamingaborted_tools——用户决定停止
  • 异常终止:其余七种——系统遇到了无法继续的情况

对于异常终止,系统在返回终止状态之前会执行清理逻辑:取消正在执行的工具、释放资源引用、记录终止原因到日志。这些清理逻辑确保了即使 Agent 异常退出,也不会留下"脏"的状态。

状态轮转

对话循环的核心状态机由两个概念驱动:可变循环状态(State)和终止信号(Terminal)。

State 类型定义了循环的完整可变状态,包含消息列表、工具使用上下文、自动压缩追踪状态、输出 token 恢复计数器、是否已尝试响应式压缩、输出 token 覆盖限制、待处理的工具摘要、stop hook 是否激活、turn 计数,以及上一次继续循环的原因。每次 continue 回到循环顶部时,都会构造一个全新的状态对象。transition 字段记录了上一次 continue 的原因,用于在恢复逻辑中避免重复执行相同的恢复路径。

Terminal 和 Continue 类型定义在独立的模块中。Terminal 标记对话的终结(携带 reason 字段),而 Continue 标记继续循环的决策(携带 reason 和可选的附加信息)。

这个三元模型(State + Continue + Terminal)的精妙之处在于它用类型系统强制了循环的正确性:

  • State 是"可变但可控"的数据容器,每次 continue 都创建新实例
  • Continue 是"继续"的信号,携带原因和附加信息,指导下一轮迭代的行为
  • Terminal 是"终止"的信号,携带原因,结束循环并返回给调用方

关键的转换路径包括:

  1. next_turn:正常的工具调用后继续。消息列表扩展为原始消息加上助手消息和工具结果,turn 计数递增。这是最常见也是最简单的转换路径。
  2. max_output_tokens_recovery:模型输出被截断时,注入恢复消息后继续循环。恢复消息指导模型从截断处继续。最多重试 3 次。这个路径的存在是因为 LLM 有时会在输出过长时被 API 截断——不是错误,而是模型"说得太多"了。恢复消息相当于告诉模型"你刚才的话说到一半被打断了,请从中断处继续"。
  3. max_output_tokens_escalate:首次截断时尝试提升输出 token 限制,而非注入恢复消息。这是一种更优雅的恢复策略——与其让模型从中断处继续,不如给它更大的输出空间,让它一次性完成。只有当提升限制后仍然截断时,才回退到 recovery 路径。
  4. reactive_compact_retry:上下文过长时,通过响应式压缩恢复。压缩失败则终止循环。这个路径是对话循环的"紧急刹车"——当所有预防性压缩手段都没能阻止上下文溢出时,reactive compact 作为最后的恢复手段尝试挽救对话。
  5. collapse_drain_retry:上下文折叠的溢出恢复路径。优先于响应式压缩执行,因为折叠保留粒度更细的上下文。这个优先级排序体现了"最小信息损失"原则——在所有恢复手段中,优先使用丢失信息最少的方法。
  6. stop_hook_blocking:Stop hook 返回阻塞错误时,将错误注入消息列表后继续,让模型有机会修正。这个路径展示了 Agent 系统的一个关键设计理念:错误不一定是终止条件,也可以是反馈信号。 模型收到 hook 的错误信息后,可能会调整策略并尝试不同的方案。
  7. token_budget_continuation:Token 预算管理触发的继续,注入一个提示消息提醒模型注意预算。这类似于手机流量套餐的"余额不足提醒"——不是立即断网,而是提醒用户注意剩余流量。

每条 continue 路径都精心构造了新的状态对象,确保不同的恢复策略之间不会冲突。transition 字段的存在使得后续迭代可以识别"我是怎么来到这里的",从而做出更智能的决策。

最佳实践: 在设计自己的 Agent 循环时,为每条 Continue 路径记录原因(transition reason)是一个简单但极其有效的调试手段。当 Agent 行为异常时,追溯 transition 链可以帮助你快速定位是哪一次转换引入了问题。

工具系统

如果 Agent 只有一个 Bash 工具,所有任务都会变成 Shell 命令——读取文件用 cat,搜索代码用 grep,编辑文件用 sed。这虽然可行,但违反了"使用正确工具解决正确问题"的工程原则。Claude Code 的工具系统提供了 45+ 个专门化工具,每个工具针对特定的操作类型做了优化——这就好比为不同任务配备不同的专业工具,而不是用一把锤子解决所有问题。

工具协议

Claude Code 的每个工具都遵循一个统一的类型契约 – Tool<Input, Output, Progress>。这个契约定义在工具类型核心模块中,是整个工具系统的基石。理解它,就理解了 Agent "双手"的解剖结构。

这个协议的设计哲学可以用"接口即架构"来概括:通过定义严格的类型接口,工具系统的所有架构约束——权限检查、并发控制、进度报告、UI 渲染——都被编译器强制执行。开发者无法"忘记"实现某个方法,因为类型检查器会立即报错。

核心类型:Tool、Tools、ToolDef、buildTool

Tool 类型是一个泛型接口,接受三个类型参数:

  • Input extends AnyObject:使用 Zod schema 定义的工具输入类型,确保每个工具的输入都是一个结构化对象。
  • Output:工具的输出类型,自由定义。
  • P extends ToolProgressData:工具的进度数据类型,用于流式反馈。

每个工具必须实现的五要素如下

要素一:名称与别名

每个工具拥有一个唯一的名称标识符,以及可选的别名用于向后兼容。当工具重命名时,旧名称可以通过别名继续匹配。工具查找函数同时检查主名称和别名。

别名机制的存在揭示了一个工程实践原则:在公开 API 中,重命名是"只增不减"的操作。 即使某个工具的名称不再准确(如从 SearchTool 重命名为 GrepTool),旧名称也必须通过别名保持可用,否则依赖旧名称的配置、脚本和用户习惯都会被打破。

要素二:Zod Schema

每个工具使用 Zod 定义其输入参数的 schema。Zod schema 承担了双重职责:

  1. 运行时验证:在工具执行之前,LLM 生成的参数经过 Zod 解析,确保类型和约束的正确性。这是"不要信任外部输入"原则的体现——LLM 的输出是不可控的,工具必须自我保护。
  2. API 通信:Zod schema 通过转换层生成 JSON Schema 发送给 API,让模型知道每个参数的含义和约束。这意味着 schema 定义就是工具的"使用说明书"——模型看到的参数描述来自 Zod schema 中的 describe() 调用。

交叉引用: Zod schema 的验证发生在第 4 章权限管线的第一阶段(validateInput),这是"安全边界内嵌"设计原则的具体体现。

要素三:权限模型

权限相关的三个方法构成了分层的权限检查管线:

  1. 第一层:输入验证(validateInput):在权限检查之前运行,用于拒绝无效输入。这是"数据合法性"检查,与权限无关。
  2. 第二层:权限检查(hasPermissionsToUseTool + checkPermissions):包含工具特定的权限逻辑。不同工具的权限检查粒度不同——Read 工具可能只检查路径是否在允许列表内,而 Bash 工具需要解析命令、评估风险等级。
  3. 第三层:运行时属性判断:影响工具的并发调度策略。例如 isConcurrencySafe() 标记工具是否可以并行执行。

三层分离的设计哲学是"关注点分离":数据验证不关心权限策略,权限策略不关心并发调度。每层只做一件事,但三层串联起来提供了完整的防护。

要素四:执行逻辑

这是工具的核心执行方法。它接收解析后的输入参数、工具使用上下文、权限检查函数、父消息引用和一个可选的进度回调。返回的结果携带输出数据和可选的上下文修改器。

上下文修改器(contextModifier)允许工具在执行后修改上下文(如更新文件缓存),这是工具影响后续行为的关键通道。例如,FileWriteTool 在写入文件后会通过 contextModifier 更新文件状态缓存,使得后续的 FileReadTool 能看到最新的文件内容。

要素五:UI 渲染

工具拥有丰富的渲染方法集合,覆盖了完整的 UI 生命周期:

  • renderToolUseMessage:工具调用开始时展示(如 “Reading src/foo.ts”)
  • renderToolUseProgressMessage:工具执行中的进度展示
  • renderToolResultMessage:工具结果展示
  • renderToolUseRejectedMessage:权限被拒绝时的展示
  • renderToolUseErrorMessage:执行出错时的展示
  • renderGroupedToolUse:多个并行工具的分组展示

每个渲染方法都返回 React.ReactNode,使得工具系统与 React 渲染管线深度集成。这个设计选择意味着工具的 UI 表现可以像 React 组件一样灵活——进度条、颜色高亮、折叠面板、表格布局,都可以通过 React 组件实现。

这六个渲染方法的覆盖范围值得注意:它涵盖了工具调用的"生老病死"——从开始(renderToolUseMessage)到进行中(renderToolUseProgressMessage),到成功(renderToolResultMessage)、被拒(renderToolUseRejectedMessage)、出错(renderToolUseErrorMessage),以及并行执行的分组展示(renderGroupedToolUse)。这种完整的生命周期覆盖确保了用户在任何状态下都能看到清晰、有意义的 UI 反馈。

工具注册和发现机制

getAllBaseTools() 完整工具清单:

getAllBaseTools() 是所有内建工具的注册中心。它返回一个扁平数组,包含了 Claude Code 所有可用的工具。通过这个函数,我们可以统计出核心工具清单,并按功能分类:

类别 工具 职责 并发安全
执行 BashTool 运行 Shell 命令 否(副作用)
文件 FileReadTool, FileEditTool, FileWriteTool 读取、编辑、写入文件 Read 是,Edit/Write 否
搜索 GlobTool, GrepTool 文件名模式匹配、内容搜索
笔记本 NotebookEditTool Jupyter Notebook 编辑
网络 WebFetchTool, WebSearchTool 获取 URL 内容、网络搜索
智能 AgentTool 子智能体入口
任务 TodoWriteTool, TaskCreateTool 等 任务管理 视具体工具而定
规划 EnterPlanModeTool, ExitPlanModeV2Tool 计划模式切换
交互 AskUserQuestionTool 向用户提问 否(需要用户响应)
技能 SkillTool 调用 slash command 技能
配置 ConfigTool 修改配置
MCP ListMcpResourcesTool, ReadMcpResourceTool MCP 资源访问
工作树 EnterWorktreeTool, ExitWorktreeTool Git worktree 管理
通知 BriefTool 消息发送
搜索发现 ToolSearchTool 延迟工具发现

那么这么多工具怎么办?就是我们的:

ToolSearchTool 延迟发现机制:

当工具数量超过一定阈值时,Claude Code 启用延迟工具发现(deferred tool discovery)。核心思路是:不在初始系统提示中发送所有工具的完整 schema,而是只发送工具名称列表,让模型通过 ToolSearchTool 按需加载。

用一个类比来理解:传统方式像是把整本百科全书放在模型面前——即使大部分内容在当前对话中用不到。延迟发现则像是给模型一个目录索引——模型知道有哪些工具可用,只在需要时翻开对应的页面查看详细参数。

ToolSearchTool 的实现遵循标准的工厂函数模式。判断一个工具是否应该被延迟的逻辑是:显式标记为总是加载的工具不延迟、MCP 工具总是延迟、工具搜索工具自身不延迟。

这个机制的核心价值在于节省 prompt 空间:当 MCP 服务器注册了数十个工具时,全部发送给 API 会消耗大量 token。延迟发现让模型只在需要时加载工具的完整 schema,显著减少了初始 prompt 的大小。

工具过滤机制:

  1. 模式过滤:根据模式过滤工具。简单模式只保留 Bash、Read、Edit;普通模式排除特殊工具。这种模式化的工具过滤确保了在受限环境中 Agent 只能使用最基本的工具集。
  2. 拒绝规则过滤:移除被 blanket deny 规则匹配的工具。
  3. 启用状态检查:过滤掉未启用的工具。
  4. 工具池组装:合并内建工具与 MCP 工具,按名称排序去重。排序的目的是确保 prompt 缓存稳定性——工具顺序变化会导致缓存失效。

基础工具解析

BashTool:

BashTool 在工具系统中的特殊地位体现在以下方面:

  • 错误传播:当 BashTool 执行失败时,会取消所有并行的 Bash 工具调用。这是因为 Bash 命令之间往往存在隐式依赖链(如 mkdir 失败后后续命令无意义)。这个设计体现了"快速失败"原则——与其让后续命令在一个已损坏的环境中继续执行并产生更多错误,不如立即停止整个批次。
  • 中断行为:BashTool 可以自定义用户中断时的行为。某些长时间运行的命令(如测试套件)可能选择阻塞而非取消。这个设计反映了对用户意图的精细理解:中断一个正在运行的 npm install 应该立即停止(用户改了主意),但中断一个测试套件可能只是用户想看当前进度(测试完成后结果仍然有价值)。
  • 语义分析:BashTool 会对命令进行 AST 解析和语义分析,判断命令是否为搜索/读取操作(isSearchOrReadCommand),用于 UI 折叠展示。这体现了"智能工具"的设计理念——工具不仅是被动执行命令的管道,还能理解命令的语义并做出相应的 UI 决策。
  • 沙盒集成:通过 --dangerouslyDisableSandbox 参数和沙盒配置,控制命令执行的安全边界。沙盒是 BashTool 的"安全网"——即使在 bypass 权限模式下,沙盒仍然可以限制命令的文件系统访问范围。

文件工具:

这三个工具构成了 Claude Code 文件操作的完整能力集。它们的分工反映了经典数据库操作的 CRUD 模式(Create/Read/Update),只是缺少了 Delete——这是一个有意为之的安全决策,因为"删除文件"是不可逆操作,通常通过 BashTool 的 rm 命令来实现,这会触发更严格的权限检查。

FileReadTool 负责读取文件内容。它维护了文件状态缓存,用于追踪哪些文件已被读取,避免重复注入内存附件。这个缓存机制是性能优化的关键——如果同一个文件被读取多次(在不同的工具调用轮次中),缓存确保只在第一次读取时触发实际的文件 I/O,后续读取直接使用缓存结果。

FileEditTool 负责精确编辑文件。它使用 old_string -> new_string 的精确替换模式,而非行号范围,确保编辑操作在文件变化时仍然正确。这个选择值得深入分析:

  • 为什么不用行号? 行号是脆弱的——如果在读取文件和编辑文件之间,另一个工具(或用户)修改了文件,行号可能已经偏移,导致编辑错误的位置。
  • 为什么用精确字符串匹配? 字符串匹配是幂等的——只要目标字符串存在于文件中,编辑就能正确定位。即使文件被部分修改,只要目标片段没有被触及,编辑就是安全的。

FileEditTool 的 isDestructive 方法会根据编辑内容判断是否为破坏性操作(如删除大量代码)。这种上下文感知的破坏性判断比简单的"写入即破坏性"标签更加精确。

FileWriteTool 负责创建或完全覆写文件。这是最"重"的文件操作,权限检查最为严格。FileWriteTool 与 FileEditTool 的区别在于作用范围——Edit 只修改文件中的特定片段,Write 可以完全覆盖文件内容。因此,Write 的权限检查标准更高。

三个工具都支持 contextModifier,在执行后更新文件状态缓存,使得后续的工具调用和内存附件注入能看到最新的文件状态。